import {Vector3} from '../../math/Vector3.js'
import {Curve} from '../core/Curve.js'

/**
 * Centripetal CatmullRom Curve - which is useful for avoiding
 * cusps and self-intersections in non-uniform catmull rom curves.
 * http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf
 *
 * curve.type accepts centripetal(default), chordal and catmullrom
 * curve.tension is used for catmullrom which defaults to 0.5
 */

/*
Based on an optimized c++ solution in
 - http://stackoverflow.com/questions/9489736/catmull-rom-curve-with-no-cusps-and-no-self-intersections/
 - http://ideone.com/NoEbVM

This CubicPoly class could be used for reusing some variables and calculations,
but for three.js curve use, it could be possible inlined and flatten into a single function call
which can be placed in CurveUtils.
*/

function CubicPoly() {
  let c0 = 0,
    c1 = 0,
    c2 = 0,
    c3 = 0

  /*
   * Compute coefficients for a cubic polynomial
   *   p(s) = c0 + c1*s + c2*s^2 + c3*s^3
   * such that
   *   p(0) = x0, p(1) = x1
   *  and
   *   p'(0) = t0, p'(1) = t1.
   */
  function init(x0, x1, t0, t1) {
    c0 = x0
    c1 = t0
    c2 = -3 * x0 + 3 * x1 - 2 * t0 - t1
    c3 = 2 * x0 - 2 * x1 + t0 + t1
  }

  return {
    initCatmullRom: function (x0, x1, x2, x3, tension) {
      init(x1, x2, tension * (x2 - x0), tension * (x3 - x1))
    },

    initNonuniformCatmullRom: function (x0, x1, x2, x3, dt0, dt1, dt2) {
      // compute tangents when parameterized in [t1,t2]
      let t1 = (x1 - x0) / dt0 - (x2 - x0) / (dt0 + dt1) + (x2 - x1) / dt1
      let t2 = (x2 - x1) / dt1 - (x3 - x1) / (dt1 + dt2) + (x3 - x2) / dt2

      // rescale tangents for parametrization in [0,1]
      t1 *= dt1
      t2 *= dt1

      init(x1, x2, t1, t2)
    },

    calc: function (t) {
      const t2 = t * t
      const t3 = t2 * t
      return c0 + c1 * t + c2 * t2 + c3 * t3
    },
  }
}

//

const tmp = new Vector3()
const px = new CubicPoly(),
  py = new CubicPoly(),
  pz = new CubicPoly()

function CatmullRomCurve3(points, closed, curveType, tension) {
  Curve.call(this)

  this.type = 'CatmullRomCurve3'

  this.points = points || []
  this.closed = closed || false
  this.curveType = curveType || 'centripetal'
  this.tension = tension !== undefined ? tension : 0.5
}

CatmullRomCurve3.prototype = Object.create(Curve.prototype)
CatmullRomCurve3.prototype.constructor = CatmullRomCurve3

CatmullRomCurve3.prototype.isCatmullRomCurve3 = true

CatmullRomCurve3.prototype.getPoint = function (t, optionalTarget) {
  const point = optionalTarget || new Vector3()

  const points = this.points
  const l = points.length

  const p = (l - (this.closed ? 0 : 1)) * t
  let intPoint = Math.floor(p)
  let weight = p - intPoint

  if (this.closed) {
    intPoint += intPoint > 0 ? 0 : (Math.floor(Math.abs(intPoint) / l) + 1) * l
  } else if (weight === 0 && intPoint === l - 1) {
    intPoint = l - 2
    weight = 1
  }

  let p0, p3 // 4 points (p1 & p2 defined below)

  if (this.closed || intPoint > 0) {
    p0 = points[(intPoint - 1) % l]
  } else {
    // extrapolate first point
    tmp.subVectors(points[0], points[1]).add(points[0])
    p0 = tmp
  }

  const p1 = points[intPoint % l]
  const p2 = points[(intPoint + 1) % l]

  if (this.closed || intPoint + 2 < l) {
    p3 = points[(intPoint + 2) % l]
  } else {
    // extrapolate last point
    tmp.subVectors(points[l - 1], points[l - 2]).add(points[l - 1])
    p3 = tmp
  }

  if (this.curveType === 'centripetal' || this.curveType === 'chordal') {
    // init Centripetal / Chordal Catmull-Rom
    const pow = this.curveType === 'chordal' ? 0.5 : 0.25
    let dt0 = Math.pow(p0.distanceToSquared(p1), pow)
    let dt1 = Math.pow(p1.distanceToSquared(p2), pow)
    let dt2 = Math.pow(p2.distanceToSquared(p3), pow)

    // safety check for repeated points
    if (dt1 < 1e-4) dt1 = 1.0
    if (dt0 < 1e-4) dt0 = dt1
    if (dt2 < 1e-4) dt2 = dt1

    px.initNonuniformCatmullRom(p0.x, p1.x, p2.x, p3.x, dt0, dt1, dt2)
    py.initNonuniformCatmullRom(p0.y, p1.y, p2.y, p3.y, dt0, dt1, dt2)
    pz.initNonuniformCatmullRom(p0.z, p1.z, p2.z, p3.z, dt0, dt1, dt2)
  } else if (this.curveType === 'catmullrom') {
    px.initCatmullRom(p0.x, p1.x, p2.x, p3.x, this.tension)
    py.initCatmullRom(p0.y, p1.y, p2.y, p3.y, this.tension)
    pz.initCatmullRom(p0.z, p1.z, p2.z, p3.z, this.tension)
  }

  point.set(px.calc(weight), py.calc(weight), pz.calc(weight))

  return point
}

CatmullRomCurve3.prototype.copy = function (source) {
  Curve.prototype.copy.call(this, source)

  this.points = []

  for (let i = 0, l = source.points.length; i < l; i++) {
    const point = source.points[i]

    this.points.push(point.clone())
  }

  this.closed = source.closed
  this.curveType = source.curveType
  this.tension = source.tension

  return this
}

CatmullRomCurve3.prototype.toJSON = function () {
  const data = Curve.prototype.toJSON.call(this)

  data.points = []

  for (let i = 0, l = this.points.length; i < l; i++) {
    const point = this.points[i]
    data.points.push(point.toArray())
  }

  data.closed = this.closed
  data.curveType = this.curveType
  data.tension = this.tension

  return data
}

CatmullRomCurve3.prototype.fromJSON = function (json) {
  Curve.prototype.fromJSON.call(this, json)

  this.points = []

  for (let i = 0, l = json.points.length; i < l; i++) {
    const point = json.points[i]
    this.points.push(new Vector3().fromArray(point))
  }

  this.closed = json.closed
  this.curveType = json.curveType
  this.tension = json.tension

  return this
}

export {CatmullRomCurve3}
